<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><title>뱅크샐러드 공식 블로그 | 기술 블로그</title><description>뱅크샐러드 공식 기술 블로그입니다. 뱅크샐러드의 기술, 문화, 행사 등 최신 소식과 함께  뱅크샐러드가 겪은 다양한 경험을 공유합니다.</description><item><title>뱅크샐러드가 게임을 만들 때 데이터 정합성을 유지하는 법 (feat. 낙관적 락)</title><link>https://blog.banksalad.com/tech/banksalad-optimistic-lock/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="postDetailsstyle__PostDetailsWrapper-sc-r1ppdr-0 jKrqzo post_details" morss_own_score="6.0" morss_score="17.0"&gt;&lt;h1&gt;뱅크샐러드가 게임을 만들 때 데이터 정합성을 유지하는 법 (feat. 낙관적 락)&lt;/h1&gt;&lt;span&gt;16 Jan, 2026&lt;/span&gt;&lt;img src="data:image/svg+xml;charset=utf-8,%3Csvg%20height='780'%20width='1170'%20xmlns='http://www.w3.org/2000/svg'%20version='1.1'%3E%3C/svg%3E"&gt;&lt;img src="https://blog.banksalad.com/static/e4522d9a355428cec297c5abc2b5efd0/a4cf4/thumnail.jpg"&gt;&lt;div class="postDetailsstyle__PostDescription-sc-r1ppdr-5 fTgjCK post_des" morss_own_score="6.0" morss_score="275.0"&gt;&lt;br&gt;
&lt;br&gt;
&lt;p&gt;안녕하세요 금융쇼핑 PA 백앤드 개발자 배지원입니다.&lt;/p&gt;
&lt;p&gt;최근 뱅크샐러드는 단순한 자산 관리를 넘어, 유저에게 즐거운 금융 경험을 제공하기 위해 게이미피케이션 요소를 적극적으로 도입하고 있습니다. 오늘 소개해 드릴 &lt;code&gt;일해라 김뱅샐&lt;/code&gt;은 뱅크샐러드 유저들이 캐릭터를 육성하며 즐겁게 자산을 관리할 수 있도록 돕는 대표적인 &lt;code&gt;방치형 앱테크 게임&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p&gt;오늘은 금융 플랫폼 내에서 앱테크 서비스를 구현하며 마주했던 &lt;code&gt;데이터 정합성&lt;/code&gt; 에 대한 고민, 그리고 이를 해결하기 위해 채택한 &lt;code&gt;재시도 로직 없는 낙관적 락&lt;/code&gt; 설계에 대해 공유하고자 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;code&gt;일해라 김뱅샐&lt;/code&gt;은 유저가 회사원 김뱅샐을 승진시키며 즐거움을 느끼고, 그 과정에서 실질적인 혜택을 얻는 서비스입니다. 유저는 에너지를 관리하며 책상의 쓰레기를 치우거나 스킬을 사용하여 게임 내 재화인 &lt;code&gt;샐(SAL)&lt;/code&gt;을 획득합니다.&lt;/p&gt;
&lt;p morss_own_score="6.0" morss_score="13.5"&gt;이때 획득하는 &lt;code&gt;샐(SAL)&lt;/code&gt; 은 단순한 게임 재화가 아니라, &lt;strong&gt;실제 원화(KRW)로 전환 가능한 실질적인 자산&lt;/strong&gt;입니다. 따라서 일해라김뱅샐 팀은 &lt;strong&gt;게임이 주는 즐거움은 유지&lt;/strong&gt;하면서도, &lt;strong&gt;금융 서비스 도메인에 걸맞게 유저의 자산이 정확하게&lt;/strong&gt; 계산될 수 있는 서비스 설계에 집중했습니다.&lt;/p&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/0be20a08df57605ad6a57a020db2af07/bbbf7/01.png"&gt;

&lt;img title="01" src="https://blog.banksalad.com/static/0be20a08df57605ad6a57a020db2af07/663f3/01.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt; 일해라 김뱅샐 메인 화면 &lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/b4ac6001cf15fd83408e25d80016661f/bbbf7/02.png"&gt;

&lt;img title="02" src="https://blog.banksalad.com/static/b4ac6001cf15fd83408e25d80016661f/663f3/02.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt; 일해라 김뱅샐의 서비스 사이클 &lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;‘일해라 김뱅샐’&lt;/strong&gt; 의 핵심 데이터인 &lt;code&gt;character_state&lt;/code&gt;(레벨, SAL, 에너지 등)는 다양한 경로를 통해 실시간으로 업데이트됩니다. 시스템을 설계하며 저희가 정의한 데이터 수정 트리거는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;유저의 직접적인 액션(API):&lt;/strong&gt; 에너지충전, 레벨업, 쓰레기 치우기, 스킬 사용 등 유저 활동에 따른 즉각적인 상태 변경.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 변수(Backoffice):&lt;/strong&gt; CS 처리나 이벤트 보상 지급 등을 위해 운영자가 백오피스에서 유저의 잔액을 수동으로 조정하는 경우.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예방적 설계:&lt;/strong&gt; 이후에 생길 수 있는 배치처리, 비동기적인 이벤트에 따른 상태 업데이트 상황 등&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;aside&gt;
&lt;div style="background-color: rgb(241,241,239); padding: 16px 16px 16px 16px; border-radius: 6px;line-height: 1.9" morss_own_score="3.0" morss_score="14.5"&gt;
&lt;p&gt;&lt;strong&gt;⚠️  데이터 정합성을 위협하는 ‘갱신 손실(Lost Update)’&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;만약 적절한 동시성 제어가 없다면, 동일한 데이터에 대해 서로 다른 수정 요청 과정에서 &lt;strong&gt;갱신 손실(Lost Update)&lt;/strong&gt; 현상이 발생할 수 있습니다. 갱신손실이란 두 개 이상의 트랜잭션이 동일한 데이터를 동시에 수정할 때, 한 트랜잭션의 변경 내용이 다른 트랜잭션에 의해 덮어쓰여져서 원래의 변경 결과가 사라지는 문제를 의미합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실제 발생 가능한 시나리오:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;유저(A)의 요청:&lt;/strong&gt; 스킬을 사용해 200 SAL을 획득하려고 시도합니다. 시스템은 유저의 현재 잔액인 &lt;strong&gt;1,000 SAL&lt;/strong&gt;을 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영자(B)의 요청:&lt;/strong&gt; 동일한 찰나에 CS 보상으로 500 SAL을 지급합니다. 시스템은 역시 현재 잔액인 &lt;strong&gt;1,000 SAL&lt;/strong&gt;을 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유저(A)의 반영:&lt;/strong&gt; 스킬 보상을 더한 &lt;strong&gt;1,200 SAL&lt;/strong&gt;로 데이터 업데이트를 완료합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영자(B)의 반영:&lt;/strong&gt; 자신의 조회 결과에 보상을 더한 &lt;strong&gt;1,500 SAL&lt;/strong&gt;로 데이터 업데이트를 완료합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;결과:&lt;/strong&gt; 유저가 스킬사용을 통해 얻은 200SAL은 최종 결과에 반영되지 못하고 &lt;strong&gt;누락&lt;/strong&gt;됩니다. 최종 잔액은 유저의 활동 기록이 무시된 채 1,500SAL로 남게 되는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3&gt;&lt;strong&gt;데이터 정합성이 무너진다는 것의 의미&lt;/strong&gt;&lt;/h3&gt;
&lt;br&gt;
&lt;p&gt;이런 레이스 컨디션(Race Condition)은 단순히 데이터가 꼬이는 문제를 넘어, 서비스 전반에 기술적 부채와 운영 리스크를 남깁니다. 내가 열심히 스킬을 쓰고 쓰레기를 치웠는데 정작 보상이 제대로 들어오지 않는다면, 유저는 금세 서비스에 대한 신뢰를 잃게 될 것입니다. 특히 현금성 재화가 오가는 시스템에서 데이터 정합성은 결코 타협할 수 없는 기본 중의 기본입니다. 결국 저희 팀에게 동시성 제어는 단순한 성능 최적화가 아니라, 유저의 소중한 활동 내역을 데이터로 온전하게 지켜내기 위한 필수적인 안전장치였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;h2&gt;&lt;strong&gt;어떤 방식을 고려했고, 왜 ‘낙관적 락’이었나요?&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;우리는 뱅크샐러드의 MSA 환경에서 선택 가능한 세 가지 주요 잠금 방식을 검토했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;후보 1: 비관적 락 (Pessimistic Lock)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DB 레벨에서 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;를 통해 행을 선점하는 방식입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징:&lt;/strong&gt; 트랜잭션이 끝날 때까지 타 프로세스의 접근을 원천 차단합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 정합성이 매우 중요하고 &lt;strong&gt;충돌이 빈번할 때&lt;/strong&gt; 확실한 대안입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 잠금을 획득하기 위한 대기가 발생하여 대규모 트래픽 상황에서 DB 커넥션 병목이 생길 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;후보 2: 분산 락 (Distributed Lock)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redis(Redlock)나 ZooKeeper 같은 외부 인프라를 활용하여 서버 인스턴스 간 자원 접근을 제어합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징:&lt;/strong&gt; DB 밖의 저장소에 ‘잠금 권한’을 기록합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 여러 DB에 걸쳐 있거나 외부 API와 연동된 복잡한 동기화에 유리합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; Redis 통신 오버헤드와 별도 인프라 운영 비용이 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;후보 3: 낙관적 락 (Optimistic Lock)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;버전 번호(&lt;code&gt;update_version&lt;/code&gt;)를 통해 수정 시점에 데이터가 변했는지 확인합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;특징:&lt;/strong&gt; 실제 업데이트 쿼리 시점에 &lt;code&gt;WHERE&lt;/code&gt; 조건으로 버전 일치 여부를 검사합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; DB 잠금 시간이 거의 없어 성능이 최상이며, 데드락 위험이 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 충돌 시 애플리케이션 레벨에서 에러를 처리해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside&gt;

&lt;p&gt;&lt;strong&gt;💡 잠깐, 이 외에도 다른 락 방식이 있을까요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위 방식 외에 특수한 상황에서 사용되는 기술들도 짧게 언급하자면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;네임드 락 (Named Lock):&lt;/strong&gt; MySQL의 &lt;code&gt;GET_LOCK()&lt;/code&gt; 함수처럼 특정 문자열에 대해 잠금을 거는 방식입니다. 메타데이터 기반의 잠금으로, 분산 락과 유사한 효과를 내면서도 별도의 Redis 없이 DB만으로 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;뮤텍스 (Mutex/Semaphore):&lt;/strong&gt; 단일 인스턴스 내의 여러 고루틴(Goroutine) 간 동기화가 필요할 때 사용합니다. 하지만 분산 환경인 뱅크샐러드 서버 구조에서는 인스턴스 간 제어가 불가능하여 고려 대상에서 제외되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스핀 락 (Spin Lock):&lt;/strong&gt; 락을 얻을 때까지 반복문을 돌며 확인하는 방식입니다. CPU 자원을 많이 소모하기 때문에 짧은 시간 내에 해제될 락이 아니라면 지양해야 합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;/aside&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;‘일해라 김뱅샐’&lt;/strong&gt; 팀은 수많은 방식 중 낙관적 락을 최종 선택했습니다. 그 이유는 서비스의 구체적인 ‘데이터 액세스 패턴’과 ‘도메인 특성’에 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;‘일해라 김뱅샐’&lt;/strong&gt; 의 캐릭터 데이터는 각 유저별로 격리된 구조입니다. 특정 유저의 데이터를 수많은 타인이 동시에 수정할 일이 없으며, 주로 유저 본인의 요청과 운영자의 간헐적인 수정이 겹치는 정도입니다. 이러한 환경에서 성능 오버헤드가 큰 비관적 락이나 분산 락은 오버 엔지니어링이라고 판단했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;별도의 Redis 인프라를 구축하고 관리해야 하는 분산 락에 비해, 낙관적 락은 기존 DB 스키마에 &lt;code&gt;update_version&lt;/code&gt; 컬럼 하나를 추가하는 것만으로 구현이 가능합니다. 시스템 복잡도를 낮게 유지하면서도 정합성을 챙길 수 있는 가장 효율적인 방법이었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;게임에서 가장 중요한 것은 유저의 흐름이 끊기지 않는 것입니다. 수많은 유저가 동시에 접속하더라도, DB Lock 대기 때문에 화면이 멈칫하거나 반응이 늦어지는 일은 없어야 한다고 생각했습니다. 보상을 획득하는 즐거운 순간에 유저의 경험을 해치지 않도록, 낙관적 락을 통해 반응성을 유지하며 유저가 캐릭터의 성장에만 몰입할 수 있는 환경을 만들고 싶었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;낙관적 락 도입을 망설이게 하는 가장 큰 요인은 보통 &lt;strong&gt;‘충돌 시 어떻게 처리 할 것인가’&lt;/strong&gt; 에 대한 결정과 복잡도 관리입니다. 자칫하면 코드가 복잡해지고, 관리포인트가 늘어나기 때문이죠. 하지만 ‘일해라 김뱅샐’은 서비스 특성상 이러한 부담이 매우 낮았습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;1. 시간 차분(Time-delta)을 활용한 보상 이월 구조&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;유저가 벌어들인 수익 계산 로직은 개별 클릭 이벤트에 의존하는 ‘이벤트 적립’ 방식이 아니라, &lt;code&gt;마지막 업데이트 시점&lt;/code&gt;과 &lt;code&gt;현재 시점&lt;/code&gt; 사이의 시간 간격(Time-delta)을 계산해 보상을 합산하는 &lt;code&gt;상태 기반 정산&lt;/code&gt; 방식을 사용합니다.&lt;/p&gt;

&lt;br&gt;
&lt;p&gt;💡 &lt;mtext&gt;Reward&lt;/mtext&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mo&gt;(&lt;/mo&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;mtext&gt;current&lt;/mtext&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;mtext&gt;last_update&lt;/mtext&gt;&lt;mo&gt;)&lt;/mo&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mtext&gt;Rate&lt;/mtext&gt;&lt;mtext&gt;per_second&lt;/mtext&gt;&lt;annotation&gt;\text{Reward} = (T_{\text{current}} - T_{\text{last\_update}}) \times \text{Rate}_{\text{per\_second}}&lt;/annotation&gt;&lt;span&gt;Reward&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;span&gt;−&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;last_update&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;×&lt;/span&gt;&lt;span&gt;Rate&lt;/span&gt;&lt;span&gt;per_second&lt;/span&gt;&lt;span&gt;​&lt;/span&gt;&lt;/p&gt;

&lt;br&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;보상의 자동 이월:&lt;/strong&gt; 특정 요청이 동시성 충돌로 실패하더라도 유저의 수익은 누락되지 않습니다. DB의 업데이트 시점이 갱신되지 않았으므로, &lt;strong&gt;다음 액션이 성공하는 시점에 실패했던 기간까지 포함하여 한꺼번에 정산&lt;/strong&gt;되기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실패의 재정의:&lt;/strong&gt; 여기서 실패는 데이터의 유실이 아니라, 단지 ‘보상 확정 시점의 일시적인 이월’ 을 의미합니다. 유저는 손해를 보지 않고, 서버는 복잡한 재시도 처리를 할 필요가 없어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;2. 자기 수정(Self-correcting) 아키텍처를 통한 리소스 최적화&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;모든 요청을 성공시키기 위해 서버에서 복잡한 재시도 루프를 돌리는 것은 DB 커넥션과 CPU 자원 낭비일 수 있습니다. 저희는 이를 억지로 해결하기보다 시스템이 스스로 수렴하도록 설계했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;자연스러운 상태 수렴&lt;/strong&gt;: 특정 업데이트가 실패하더라도, 이어지는 유저의 다음 액션이나 시스템 이벤트가 항상 최신 상태를 기준으로 다시 동작합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결과적 일관성&lt;/strong&gt;: 별도의 보정 로직 없이도 시스템은 결국 최신 정산 상태로 자기 수정(Self-correcting) 하며 수렴하게 됩니다. 이는 분산 환경에서 시스템의 복잡도를 낮추면서도 높은 신뢰도를 유지하는 핵심 전략입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2&gt;&lt;strong&gt;결론&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;결국 저희는 복잡한 락 메커니즘을 도입하기보다, 도메인의 흐름을 이용해 기술적 문제를 우회한&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“가장 단순하면서도 서비스 특성에 딱 맞는”&lt;/strong&gt; &lt;code&gt;낙관적 락&lt;/code&gt;을 통해 성능과 정합성을 보장할 수 있도록했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;우리는 DB 스키마에 &lt;code&gt;update_version&lt;/code&gt; 이라는 필드를 추가하고, 애플리케이션 레이어에서 이를 검증하도록 구현했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;데이터베이스의 I/O 효율을 높이고 Lock 범위를 좁히기 위해 정적 데이터(&lt;code&gt;character&lt;/code&gt;)와 동적 데이터(&lt;code&gt;character_state&lt;/code&gt;)를 분리했습니다. 이는 단순히 테이블 수의 증가가 아니라, &lt;strong&gt;인덱스 리프 노드의 경합을 줄이는 설계적 장치&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 업데이트가 빈번한 데이터들을 분리&lt;/span&gt;
&lt;span&gt;CREATE&lt;/span&gt; &lt;span&gt;TABLE&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;character_state&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;character_state_id&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;BIGINT&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt; &lt;span&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span&gt;COMMENT&lt;/span&gt; &lt;span&gt;'캐릭터 상태 ID'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;character_id&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;BIGINT&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;current_sal&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;BIGINT&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt; &lt;span&gt;COMMENT&lt;/span&gt; &lt;span&gt;'경험치이자 자산인 SAL'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;current_energy&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;BIGINT&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt; &lt;span&gt;COMMENT&lt;/span&gt; &lt;span&gt;'캐릭터가 돈을 벌어들일 수 있는 에너지(초 단위)'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;update_version&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;BIGINT&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt; &lt;span&gt;COMMENT&lt;/span&gt; &lt;span&gt;'동시성 제어를 위한 버전 필드'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;&lt;span&gt;`&lt;/span&gt;updated_at&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;DATETIME&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt; &lt;span&gt;DEFAULT&lt;/span&gt; &lt;span&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span&gt;ON&lt;/span&gt; &lt;span&gt;UPDATE&lt;/span&gt; &lt;span&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;PRIMARY&lt;/span&gt; &lt;span&gt;KEY&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;character_state_id&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;UNIQUE&lt;/span&gt; &lt;span&gt;KEY&lt;/span&gt; &lt;span&gt;&lt;span&gt;`&lt;/span&gt;idx_character_u1&lt;span&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;&lt;span&gt;`&lt;/span&gt;character_id&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt; &lt;span&gt;ENGINE&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;InnoDB&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;u &lt;span&gt;*&lt;/span&gt;stateUpdater&lt;span&gt;)&lt;/span&gt; &lt;span&gt;UpdateState&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx context&lt;span&gt;.&lt;/span&gt;Context&lt;span&gt;,&lt;/span&gt; exec boil&lt;span&gt;.&lt;/span&gt;ContextExecutor&lt;span&gt;,&lt;/span&gt; characterID &lt;span&gt;int64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; updateVersion &lt;span&gt;int64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; state &lt;span&gt;*&lt;/span&gt;State&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;int64&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    newUpdateVersion &lt;span&gt;:=&lt;/span&gt; updateVersion &lt;span&gt;+&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;
    &lt;span&gt;// ... 업데이트 필드 설정 ...&lt;/span&gt;

    numUpdated&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; mysql&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CharacterStates&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
        mysql&lt;span&gt;.&lt;/span&gt;CharacterStateWhere&lt;span&gt;.&lt;/span&gt;CharacterID&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EQ&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;characterID&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        mysql&lt;span&gt;.&lt;/span&gt;CharacterStateWhere&lt;span&gt;.&lt;/span&gt;UpdateVersion&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EQ&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;updateVersion&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 버전 체크!&lt;/span&gt;
    &lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UpdateAll&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx&lt;span&gt;,&lt;/span&gt; exec&lt;span&gt;,&lt;/span&gt; m&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;if&lt;/span&gt; numUpdated &lt;span&gt;==&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; ErrConcurrentStateUpdate &lt;span&gt;// 충돌 감지&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;return&lt;/span&gt; newUpdateVersion&lt;span&gt;,&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🧐 [Tip] updated_at을 버전 필드로 쓰면 안 될까?&lt;/strong&gt;
실무에서는 반드시 &lt;strong&gt;정수형 버전 필드&lt;/strong&gt;를 따로 써야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;시간 정밀도의 한계&lt;/strong&gt;: DB 타임스탬프가 밀리초(ms) 단위일 경우, &lt;strong&gt;동일 밀리초 내 두 번의 업데이트&lt;/strong&gt;가 발생하면 충돌을 감지하지 못합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;클럭 스큐(Clock Skew)&lt;/strong&gt;: 분산 환경의 서버 간 미세한 시간 차이는 논리적 선후 관계를 보장하기 어렵게 만듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;h2&gt;&lt;strong&gt;실전 운영 가이드&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;낙관적 락은 구현이 간단해 보이지만, 실제 분산 환경에서 운영하다 보면 예상치 못한 데이터 정합성 이슈나 성능 저하를 마주하게 됩니다. &lt;strong&gt;‘일해라 김뱅샐’&lt;/strong&gt; 서비스의 안정성을 담보하기 위해 반드시 고려해야 할 실무 포인트들을 정리했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3&gt;&lt;strong&gt;1) 복제 지연(Replication Lag)과 Stale Read 대응&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;분산 DB 아키텍처에서 Master(Primary)의 데이터 업데이트가 Replica(Secondary)로 전파되는 데에는 불가피한 시차가 발생합니다. 만약 낡은 데이터(Stale Data)를 읽어 수정을 시도하면 버전 충돌률이 급격히 높아집니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Consistent Read 강제&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;자산 업데이트나 상태 변경처럼 정합성이 크리티컬한 경로에서는 반드시 &lt;strong&gt;Primary DB 조회를 강제&lt;/strong&gt;하여 최신 버전을 읽어야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;이벤트 대조 핸들러&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이벤트 기반 아키텍처라면 메시지에 포함된 버전과 DB 현재 버전을 비교하고, DB가 아직 업데이트 전이라면 전파가 완료될 때까지 짧은 대기 후 다시 시도하는 핸들러 설계가 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3&gt;&lt;strong&gt;2) 재시도 전략: 충돌은 ‘에러’가 아닌 ‘제어 흐름’&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;낙관적 락에서 충돌(Conflict)은 시스템 장애가 아닌 &lt;strong&gt;정상적인 제어 흐름&lt;/strong&gt;의 일부입니다. 따라서 모든 충돌에 무조건적인 재시도를 수행하기보다, 비즈니스 성격에 맞는 정교한 재시도 전략이 필요합니다.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;재시도 구현 시 검토해야 할 4원칙&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fresh Read의 원칙 (최신 데이터 재조회)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;재시도 루프 내부에서는 반드시 Primary DB에서 최신 버전 정보를 다시 읽어와야 합니다. 메모리에 로드되어 있던 기존 객체를 그대로 들고 재시도하는 것은 무의미한 연쇄 충돌을 야기하고 CPU 자원을 낭비할 뿐입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;지수 백오프와 지터&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;충돌 직후 즉시 재시도하면 또다시 충돌할 확률이 매우 높습니다. 재시도 간격을 &lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;annotation&gt;2^n&lt;/annotation&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt; 형태로 늘리는 지수 백오프를 적용하고, 여기에 무작위 값인 지터(Jitter)를 섞어 요청 시점을 분산시켜야 합니다. 이는 수많은 요청이 한꺼번에 몰리는 현상을 방지하여 DB의 병목을 막아주는 핵심 기술입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;멱등성(Idempotency) 보장&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;서버 내부의 재시도가 중복 처리되거나, 네트워크 이슈로 응답을 못 받은 클라이언트가 재요청을 보낼 위험이 있습니다. 클라이언트가 생성한 request_id를 기반으로 멱등성 테이블을 운용하여, 동일 요청에 대해서는 실제 DB 쓰기 없이 이전의 성공 결과를 반환하는 안전장치가 병행되어야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;최대 재시도 횟수 제한&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;낙관적 락의 재시도는 무한정 이루어져서는 안 됩니다. 일정 횟수 이상 충돌이 반복된다면 이는 시스템의 심각한 경합 상황으로 판단해야 합니다. 이때는 사용자에게 명확한 에러를 반환하거나, 비즈니스 로직에 따라 비관적 락 등으로의 전환을 검토해야 하는 신호입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;지금까지 &lt;strong&gt;일해라 김뱅샐&lt;/strong&gt; 팀이 데이터 무결성을 위해 낙관적 락을 도입한 배경과 실무적인 구현 과정을 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;실제 서비스 운영 이후, 다양한 환경의 유저들로부터 수많은 요청이 발생했지만 데이터 정합성 이슈는 단 &lt;strong&gt;0건&lt;/strong&gt;이었습니다. 유저들은 게임의 재미에 몰입하면서도 자신의 활동에 따른 보상을 1원(SAL)의 오차도 없이 정확하게 받아보고있습니다.&lt;/p&gt;
&lt;p&gt;비즈니스 요구사항은 언제나 변합니다. &lt;strong&gt;저희 팀은 &lt;strong&gt;일해라 김뱅샐&lt;/strong&gt; 의 성장 가능성과 범위가 무한하다는 것을 알기에 항상 ‘그때는 맞고, 지금은 틀리다’ 라는&lt;/strong&gt; 마음가짐으로 지금 기획에 가장 최적화된 구현을 하지만 나중에 수정된 요구사항에도 유연하게 대응할 수 있는 구조를 고민하고있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</ns0:encoded></item><item><title>React랑 Lottie로 게임을 만든다고요?</title><link>https://blog.banksalad.com/tech/banksalad-react-lottie/</link></item><item><title>2025 re:Invent 여정</title><link>https://blog.banksalad.com/tech/aws-reinvent-2025/</link></item><item><title>우리가 테스트를 하는 이유. 근데 이제 Golang을 곁들인</title><link>https://blog.banksalad.com/tech/why-we-do-test-by-golang/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="postDetailsstyle__PostDetailsWrapper-sc-r1ppdr-0 jKrqzo post_details" morss_own_score="5.99023199023199" morss_score="16.98512125769366"&gt;&lt;h1&gt;우리가 테스트를 하는 이유. 근데 이제 Golang을 곁들인&lt;/h1&gt;&lt;span&gt;15 Dec, 2025&lt;/span&gt;&lt;img src="data:image/svg+xml;charset=utf-8,%3Csvg%20height='780'%20width='1170'%20xmlns='http://www.w3.org/2000/svg'%20version='1.1'%3E%3C/svg%3E"&gt;&lt;img src="https://blog.banksalad.com/static/a457b85beb8459bc9cd322208621ca7b/a4cf4/cover.jpg"&gt;&lt;div class="postDetailsstyle__PostDescription-sc-r1ppdr-5 fTgjCK post_des" morss_own_score="5.989778534923339" morss_score="285.96346274544965"&gt;&lt;p&gt;안녕하세요, 금융쇼핑 PA Server Engineer 김한수입니다. 이 글에서는 “우리는 왜 테스트를 하는지?”에 대한 질문에 좀 더 깊숙이 들어가 보고 실제 Golang에서 테스트를 도와주는 도구들에 관해서 이야기해보려고 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;우리가 엔지니어링에서 테스트하는 목적은 무엇일까요? &lt;strong&gt;회귀 버그 방지, 코드 품질 향상&lt;/strong&gt; 등 테스트 자동화가 주는 가치에 대해서는 널리 퍼져있지만, 그 사고 과정과 본질에 대해서 다루는 이야기는 많지 않아 아쉬운 것 같습니다. 그래서 이 글에서는 조심스럽게 그 본질에 대해서 이야기해 보려고 해요.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;테스트의 본질을 이해하려면 먼저 “테스트는 어떻게 탄생했을까?”에 대한 답을 해봐야 할 것 같습니다.&lt;/p&gt;
&lt;p&gt;이건 우리가 살아가는 세상의 원리와 어느 정도 맞닿아 있는데요,
이 세상의 대부분 도구가 탄생하는 과정은 &lt;code&gt;어떤 문제&lt;/code&gt;를 발견하는 걸 시작으로, 그 문제를 해결하기 위해서 &lt;code&gt;해결 방안(Solution)&lt;/code&gt;을 도출한 뒤에 그것을 만족하는 &lt;code&gt;구체적인 방법(=도구)&lt;/code&gt;을 찾는 과정을 거치게 됩니다.&lt;/p&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/056c3b41262c5df8cc2ed3a93ea6b719/fdb5a/1_01.jpg"&gt;

&lt;img title="1_01" src="https://blog.banksalad.com/static/056c3b41262c5df8cc2ed3a93ea6b719/8ea4d/1_01.jpg"&gt;
&lt;/a&gt;

&lt;br&gt;
&lt;br&gt;
&lt;p&gt;여기서 “테스트한다”는 &lt;code&gt;해결 방안(Solution)&lt;/code&gt;에 속하고 &lt;code&gt;구체적인 방법&lt;/code&gt;에는 여러 테스트 방법론이 해당한다고 생각됩니다. 그렇다면 좀 더 근본적으로 들어가서 테스트라는 해결 방안은 어떤 문제를 해결하기 위해 도출되었을까요?&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;테스트가 해결하려는 문제를 알아보기 위해 한 가지 예시를 들어보겠습니다.&lt;/p&gt;
&lt;p&gt;여러분 중에 대부분은 아마도 ‘내가 이때 출발하면 목적지에 늦지 않게 도착할 수 있을까?’라는 질문이 생긴 경험이 있을 겁니다. 그리고 이 질문을 해소하기 위해 미리 길 찾기 기능을 사용해 보곤 합니다.
우리는 이처럼 불확실함을 해소하기 위해 미리 Test 해보는 과정을 자연스럽게 경험합니다.&lt;/p&gt;
&lt;p&gt;이걸 좀 더 근본적으로 바라보면, 인간은 &lt;code&gt;불확실성&lt;/code&gt;에서 오는 불안감을 해결하고 싶어 하는 경향이 있는 걸 알 수 있습니다. 그래서 &lt;strong&gt;데이터를 모으고 미래를 예측(Test)함으로써 불안함을 완화&lt;/strong&gt;하는 모습을 보입니다.
하지만 안타깝게도 이 과정을 거친다고 해서 우리의 의도대로 세상이 100% 돌아가는 건 아닙니다.
다만, 그 확률을 100%에 수렴하게 만드는 것에 의미가 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;위에 살펴본 예시처럼 우리는 무의식적으로 데이터를 모으고 테스트를 통해 검증하는 &lt;code&gt;피드백 순환 고리(Feedback Loop)&lt;/code&gt;를 만들고 있습니다.
이건 개인의 삶뿐만 아니라 비즈니스에서도 꽤 중요하고 중간에 길을 잃더라도 다시 푯대를 향해 나아갈 수 있게 도와주는 유용한 시스템입니다.&lt;/p&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/fa933b4cb033dd381016bb9a639d055e/fdb5a/1_02.jpg"&gt;

&lt;img title="1_01" src="https://blog.banksalad.com/static/fa933b4cb033dd381016bb9a639d055e/8ea4d/1_02.jpg"&gt;
&lt;/a&gt;

&lt;br&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 엔지니어링에서 테스트는 어떤 확률을 높이기 위해 활용되는 걸까요?&lt;/p&gt;
&lt;p&gt;먼저, 기술 관점에서 테스트의 목적을 떠올려보면 자연스럽게 &lt;code&gt;회귀 버그 방지&lt;/code&gt;, &lt;code&gt;비즈니스 요구사항 만족&lt;/code&gt;이 떠오르는 것 같습니다. 그리고 메이커의 관점에서 생각해보면 결국 ‘비즈니스 리스크를 줄이기 위해서’라는 결론으로 이어진다고 생각합니다. 이것에 대해서는 아래에서 좀 더 이야기해 보려고 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;소프트웨어 개발 중 신규 개발 또는 리팩토링을 통한 코드 수정 시에 &lt;code&gt;회귀 버그(Regression Bug)&lt;/code&gt;가 발생할 수 있습니다.
&lt;code&gt;회귀 버그&lt;/code&gt;는 ‘정상적으로 동작하던 소프트웨어가 의도치 않게 오작동하는 것’을 의미하고, 이는 유저 경험을 해치거나 비즈니스 리스크로 적용될 수 있습니다.&lt;/p&gt;
&lt;p&gt;실제 상황을 한 번 상상해 보죠.
만약 리팩토링 후 버그가 발생했는데, 그 기능이 1분에 몇백만 원의 매출을 만드는 중요한 기능이고 자동화된 테스트가 없어서 이 문제를 실제 버그가 발생한 지 5분 뒤에 발견했다면 어땠을까요?
5분이라는 짧은 시간이지만 길게 느껴지고 정말 아찔한 순간일 겁니다.
장애를 해소한 후에 팀원의 격려와 위로를 받겠지만, 개인적으로는 다시는 겪고 싶지 않은 상황일 거예요.&lt;/p&gt;
&lt;p&gt;그렇다면 반대로 ‘그런 중요한 기능을 왜 건드려?’라고 생각할 수도 있을 것 같습니다.
하지만, 소프트웨어를 적절한 시기에 리팩토링하지 않는다면 개발 속도가 점점 느려지고 누적되어 결국 비즈니스 속도가 느려지게 될 겁니다. 그래서 자동화된 테스트 코드를 통해 소프트웨어를 안정적으로 리팩토링할 수 있는 유연성을 확보하는 것은 중요하다고 생각됩니다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;회귀 버그&lt;/code&gt;를 방지하는 것의 목적은 결국 비즈니스 리스크를 줄이는 것과 연결되어 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;code&gt;비즈니스 요구사항 만족&lt;/code&gt;은 말 그대로 소프트웨어가 요구사항에 맞게 의도대로 동작하는지 검증하는 것입니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 배포까지 1일 정도 남은 QA 과정에서 실제 비즈니스 요구사항과 소프트웨어의 동작이 일치하지 않는 이슈를 발견하는 상황을 상상해 봅시다. 이 경우 배포 시점을 늦춰야 할 수도 있고 누군가가 야근을 해서 대응하는 경우도 생길 수 있습니다. 하지만 이건 팀의 관점에서 모두가 소중한 선수이기 때문에 좋은 상황은 아니라고 생각됩니다.&lt;/p&gt;
&lt;p&gt;반면에 개발 과정에서 이 비즈니스 요구사항을 테스트 코드로 작성해 두고 사전에 검증했다면 어땠을까요?&lt;/p&gt;
&lt;p&gt;현실적으로 미래를 완벽히 알 수 없고 모든 케이스를 방지하는 건 어렵겠지만, 적어도 사전에 정의한 요구사항은 더 빠른 시점에 검증할 수 있었을 겁니다.&lt;/p&gt;
&lt;p&gt;결국, 이렇게 서비스 기획부터 배포까지의 과정이 늦어지는 것도 비즈니스의 속도가 늦어지고 리스크에 해당한다고 생각됩니다. 그래서 이 리스크를 방지하기 위해 우리에게는 &lt;code&gt;피드백 순환 고리&lt;/code&gt;가 필요하고 &lt;strong&gt;자동화된 테스트&lt;/strong&gt;는 개발 과정에서 &lt;code&gt;피드백 순환 고리&lt;/code&gt;로 동작하게 됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;지금까지 엔지니어링에서 테스트의 목적은 ‘비즈니스를 위해서’라는 이야기를 쭉 풀어봤습니다.
이 기반 지식에서 이제는 실질적으로 Go에서 어떤 테스트 도구들이 있는지 살펴보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;Go 언어가 가진 강력함은 ‘단순함’에서 나온다고 생각됩니다. 어려운 말을 단순하고 쉽게 하는 것이 어렵듯이 단순함은 때때로 강력합니다.&lt;/p&gt;
&lt;p&gt;이 단순함을 최대한 유지하면서 최소한의 핵심 라이브러리를 곁들이면 생산성을 크게 높일 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3&gt;&lt;strong&gt;Table Driven Test (테이블 주도 테스트)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;**Table Driven Test (테이블 주도 테스트)**는 ****&lt;a href="https://go.dev/wiki/TableDrivenTests"&gt;Go Wiki&lt;/a&gt;에도 권장하는 테스트 패턴으로, 테이블 구조로 테스트 케이스를 추가해 중복 코드를 줄이고 새로운 케이스를 추가하기 쉽습니다.&lt;/p&gt;
&lt;p&gt;다만, 개인적인 생각으로는 &lt;strong&gt;Table Driven Test&lt;/strong&gt;는 단순한 함수에서는 유용하지만 실제 의존성이 다양한 비즈니스 로직에서는 사용하기 어려운 것 같습니다. 뱅크샐러드에서는 의존성이 다양한 복잡한 테스트의 경우 다른 &lt;code&gt;stretchr/testify/suite&lt;/code&gt; package를 사용하는데요, 이건 좀 더 아래에서 이야기해 보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;"testing"&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b &lt;span&gt;int&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;int&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; a &lt;span&gt;+&lt;/span&gt; b
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestAdd&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	tests &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;struct&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		name     &lt;span&gt;string&lt;/span&gt;
		a&lt;span&gt;,&lt;/span&gt; b     &lt;span&gt;int&lt;/span&gt;
		expected &lt;span&gt;int&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;{&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"양수_더하기"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; expected&lt;span&gt;:&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		&lt;span&gt;{&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"음수_더하기"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; expected&lt;span&gt;:&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		&lt;span&gt;{&lt;/span&gt;name&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"양수와_음수_혼합"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; a&lt;span&gt;:&lt;/span&gt; &lt;span&gt;-&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; b&lt;span&gt;:&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		&lt;span&gt;// 추가할 케이스가 있다면 단순히 여기 추가&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	&lt;span&gt;for&lt;/span&gt; &lt;span&gt;_&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; tt &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;range&lt;/span&gt; tests &lt;span&gt;{&lt;/span&gt;
		t&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;tt&lt;span&gt;.&lt;/span&gt;name&lt;span&gt;,&lt;/span&gt; &lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			result &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;Add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;tt&lt;span&gt;.&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; tt&lt;span&gt;.&lt;/span&gt;b&lt;span&gt;)&lt;/span&gt;
			&lt;span&gt;if&lt;/span&gt; result &lt;span&gt;!=&lt;/span&gt; tt&lt;span&gt;.&lt;/span&gt;expected &lt;span&gt;{&lt;/span&gt;
				t&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"Add(%d, %d) = %d; expected %d"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; tt&lt;span&gt;.&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; tt&lt;span&gt;.&lt;/span&gt;b&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;,&lt;/span&gt; tt&lt;span&gt;.&lt;/span&gt;expected&lt;span&gt;)&lt;/span&gt;
			&lt;span&gt;}&lt;/span&gt;
		&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;Go에서 &lt;code&gt;*testing.T&lt;/code&gt;의 Run() 함수를 호출하면 subtest로 취급되고 내부적으로는 별도의 고루틴 안에서 동작하게 됩니다. (이하 subtest라고 표현하겠습니다.)&lt;/p&gt;
&lt;p&gt;소소한 팁으로 subtest name에 space(공백) 대신 underscore(_)를 추가하면 IDE에서 해당 테스트 코드를 검색해 찾기 쉬워집니다.&lt;/p&gt;
&lt;p&gt;실제로, subtest name에 언더바가 아닌 space를 넣고 go test를 실행하면 아래처럼 space가 underscore로 변환되어 output이 출력됩니다. 그래서 space 대신 underscore를 사용하면 복사해서 찾기 쉬워집니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt;=&lt;/span&gt; RUN   TestAdd
&lt;span&gt;==&lt;/span&gt;&lt;span&gt;=&lt;/span&gt; RUN   TestAdd/양수_더하기
--- PASS: TestAdd/양수_더하기 &lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;.00s&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;==&lt;/span&gt;&lt;span&gt;=&lt;/span&gt; RUN   TestAdd/음수_더하기
--- PASS: TestAdd/음수_더하기 &lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;.00s&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;==&lt;/span&gt;&lt;span&gt;=&lt;/span&gt; RUN   TestAdd/양수와_음수_혼합
--- PASS: TestAdd/양수와_음수_혼합 &lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;.00s&lt;span&gt;)&lt;/span&gt;
--- PASS: TestAdd &lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;.00s&lt;span&gt;)&lt;/span&gt;
PASS&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;code&gt;stretchr/testify&lt;/code&gt; 라이브러리에서 &lt;code&gt;assert&lt;/code&gt;, &lt;code&gt;require&lt;/code&gt; package를 사용하면 assertion 코드 작성 시 생산성을 높일 수 있습니다. 각 package에 대한 설명은 다음과 같습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;기능&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;assert 패키지&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;require 패키지&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;실패 시 동작&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t.Error()&lt;/code&gt; 호출&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t.FailNow()&lt;/code&gt; 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;실행 흐름&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;에러 로그를 남기고 &lt;strong&gt;계속 실행함&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;에러 로그를 남기고 &lt;strong&gt;즉시 중단함&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;주요 용도&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;값 비교, 독립적인 상태 검증&lt;/td&gt;
&lt;td&gt;필수 전제 조건(Precondition) 검증, 에러 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;더 자세한 내용을 예시 코드를 보면서 살펴보겠습니다. 아래는 기본 testing package만 사용해서 작성한 테스트 코드입니다. if문이 여러 개 붙어서 코드가 길어지는 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;"testing"&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestCreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	user&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"김뱅샐"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; err &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		t&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Fatalf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"failed to create user: %v"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Role &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;"member"&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		t&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"expected role 'member', got '%s'"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Role&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Name &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;"김뱅샐"&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		t&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Errorf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"expected name '김뱅샐', got '%s'"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Name&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래는 &lt;code&gt;stretchr/testify&lt;/code&gt;를 사용한 코드입니다. 기존 코드와 비교하면 if문이 사라지고 훨씬 간결해진 걸 확인해 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;"testing"&lt;/span&gt;
	
	&lt;span&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
	&lt;span&gt;"github.com/stretchr/testify/require"&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestCreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	user&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"김뱅샐"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	
	&lt;span&gt;// 에러가 발생하면 user는 nil이므로 아래 실행될 코드에서 nil pointer 방지를 위해 require 사용&lt;/span&gt;
	require&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;
	
	&lt;span&gt;// 마지막 파라미터로 string을 입력해 주석을 추가할 수 있음.&lt;/span&gt;
	assert&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Equal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"member"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Role&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"역할은 member 여야 함"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	assert&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Equal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"김뱅샐"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;.&lt;/span&gt;Name&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한, 아래처럼 struct끼리 비교도 간편하게 가능합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;"testing"&lt;/span&gt;
	
	&lt;span&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
	&lt;span&gt;"github.com/stretchr/testify/require"&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestCreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;// given&lt;/span&gt;
	userName &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;"김뱅샐"&lt;/span&gt;

	&lt;span&gt;// when&lt;/span&gt;
	user&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userName&lt;span&gt;)&lt;/span&gt;
	
	&lt;span&gt;// then&lt;/span&gt;
	require&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;
	expectedUser &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;User&lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;// 입력한 유저 이름, Role은 member가 반환되어야 함&lt;/span&gt;
		Name&lt;span&gt;:&lt;/span&gt; userName&lt;span&gt;,&lt;/span&gt;
		Role&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"member"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	assert&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Equal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; expectedUser&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;	&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추가로, struct Equal 검증에 실패하면 아래와 같이 output으로 diff를 보여줘서 이를 기반으로 디버깅할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error:      	Not equal: 
            	expected: &lt;span&gt;&amp;amp;&lt;/span&gt;user.User&lt;span&gt;{&lt;/span&gt;Name:&lt;span&gt;"김뱅샐"&lt;/span&gt;, Role:&lt;span&gt;"admin"&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
            	actual  &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;user.User&lt;span&gt;{&lt;/span&gt;Name:&lt;span&gt;"김뱅샐"&lt;/span&gt;, Role:&lt;span&gt;"member"&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
            	
            	Diff:
            	--- Expected
            	+++ Actual
            	@@ -2,3 +2,3 @@
            	  Name: &lt;span&gt;(&lt;/span&gt;string&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;len&lt;span&gt;=&lt;/span&gt;&lt;span&gt;9&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;"김뱅샐"&lt;/span&gt;,
            	- Role: &lt;span&gt;(&lt;/span&gt;string&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;len&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;"admin"&lt;/span&gt;
            	+ Role: &lt;span&gt;(&lt;/span&gt;string&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;len&lt;span&gt;=&lt;/span&gt;&lt;span&gt;6&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;"member"&lt;/span&gt;
            	 &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
Test:       	TestCreateUser&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;code&gt;stretchr/testify&lt;/code&gt; 로 Equal 검증 실패 시 출력되는 diff output에 단점이 일부 존재합니다.&lt;/p&gt;
&lt;p&gt;예를 들면 time.Time을 필드로 사용할 때 &lt;code&gt;testify&lt;/code&gt; 는 이를 struct로 보고 내부 필드까지 모두 비교합니다. 그래서 Equal 검증에 실패한 경우 아래와 같이 조금 어려운 test output을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error:      	Not equal: 
            	expected: &lt;span&gt;&amp;amp;&lt;/span&gt;user.User&lt;span&gt;{&lt;/span&gt;Name:&lt;span&gt;"김뱅샐"&lt;/span&gt;, Role:&lt;span&gt;"member"&lt;/span&gt;, RegisteredAt:time.Date&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2025&lt;/span&gt;, time.December, &lt;span&gt;1&lt;/span&gt;, &lt;span&gt;1&lt;/span&gt;, &lt;span&gt;36&lt;/span&gt;, &lt;span&gt;24&lt;/span&gt;, &lt;span&gt;845727000&lt;/span&gt;, time.Local&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
            	actual  &lt;span&gt;:&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;user.User&lt;span&gt;{&lt;/span&gt;Name:&lt;span&gt;"김뱅샐"&lt;/span&gt;, Role:&lt;span&gt;"member"&lt;/span&gt;, RegisteredAt:time.Date&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2024&lt;/span&gt;, time.June, &lt;span&gt;10&lt;/span&gt;, &lt;span&gt;12&lt;/span&gt;, &lt;span&gt;0&lt;/span&gt;, &lt;span&gt;0&lt;/span&gt;, &lt;span&gt;0&lt;/span&gt;, time.UTC&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
            	
            	Diff:
            	--- Expected
            	+++ Actual
            	@@ -4,13 +4,5 @@
            	  RegisteredAt: &lt;span&gt;(&lt;/span&gt;time.Time&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            	-  wall: &lt;span&gt;(&lt;/span&gt;uint64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;13998096397663520024&lt;/span&gt;,
            	-  ext: &lt;span&gt;(&lt;/span&gt;int64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;1393209&lt;/span&gt;,
            	-  loc: &lt;span&gt;(&lt;/span&gt;*time.Location&lt;span&gt;)&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
            	-   name: &lt;span&gt;(&lt;/span&gt;string&lt;span&gt;)&lt;/span&gt; &lt;span&gt;""&lt;/span&gt;,
            	-   zone: &lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;time.zone&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt;nil&lt;span&gt;&amp;gt;&lt;/span&gt;,
            	-   tx: &lt;span&gt;(&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;time.zoneTrans&lt;span&gt;)&lt;/span&gt; &lt;span&gt;&amp;lt;&lt;/span&gt;nil&lt;span&gt;&amp;gt;&lt;/span&gt;,
            	-   extend: &lt;span&gt;(&lt;/span&gt;string&lt;span&gt;)&lt;/span&gt; &lt;span&gt;""&lt;/span&gt;,
            	-   cacheStart: &lt;span&gt;(&lt;/span&gt;int64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;,
            	-   cacheEnd: &lt;span&gt;(&lt;/span&gt;int64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;,
            	-   cacheZone: &lt;span&gt;(&lt;/span&gt;*time.zone&lt;span&gt;)&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;nil&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            	-  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            	+  wall: &lt;span&gt;(&lt;/span&gt;uint64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;,
            	+  ext: &lt;span&gt;(&lt;/span&gt;int64&lt;span&gt;)&lt;/span&gt; &lt;span&gt;63853617600&lt;/span&gt;,
            	+  loc: &lt;span&gt;(&lt;/span&gt;*time.Location&lt;span&gt;)&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;nil&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
            	  &lt;span&gt;}&lt;/span&gt;
Test:       	TestCreateUser&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;test output의 가독성이 안 좋으면, 테스트를 고치는 시간이 오래 걸릴 수 있고 이는 생산성 저하로 이어진다고 생각됩니다. 그래서 이를 완화하기 위해서 아래처럼 &lt;code&gt;google/go-cmp/cmp&lt;/code&gt; 를 사용해 기존 코드보다 diff를 깔끔하게 출력할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestCreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;// given&lt;/span&gt;
	&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
		userName     &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"김뱅샐"&lt;/span&gt;
		registeredAt &lt;span&gt;=&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2024&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;6&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;12&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;UTC&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;)&lt;/span&gt;

	&lt;span&gt;// when&lt;/span&gt;
	user&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;CreateUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userName&lt;span&gt;,&lt;/span&gt; registeredAt&lt;span&gt;)&lt;/span&gt;

	&lt;span&gt;// then&lt;/span&gt;
	require&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;
	expectedUser &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;User&lt;span&gt;{&lt;/span&gt;
		Name&lt;span&gt;:&lt;/span&gt;         &lt;span&gt;"김뱅샐"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		Role&lt;span&gt;:&lt;/span&gt;         &lt;span&gt;"member"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		RegisteredAt&lt;span&gt;:&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Now&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	&lt;span&gt;// 깔끔한 diff 출력을 위해 cmp.Diff() 사용&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; diff &lt;span&gt;:=&lt;/span&gt; cmp&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Diff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;expectedUser&lt;span&gt;,&lt;/span&gt; user&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; diff &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;""&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		assert&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Failf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"expectedUser not equal to user (-expected +user):\n"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; diff&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/* 이전 코드보다 가독성이 좋아진 OUTPUT

// ...
  	Error:      	expectedUser not equal to user (-expected +user):
  	Test:       	TestCreateUser
  	Messages:   	  &amp;amp;user.User{
  	            	  	Name:         "김뱅샐",
  	            	  	Role:         "member",
  	            	- 	RegisteredAt: s"2025-12-01 01:39:56.351955 +0900 KST m=+0.001214001",
  	            	+ 	RegisteredAt: s"2024-06-10 12:00:00 +0000 UTC",
  	            	  }
--- FAIL: TestCreateUser (0.00s)

*/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;code&gt;google/go-cmp/cmp&lt;/code&gt;를 사용할 때 cmp.Option interface를 통해 Custom Comparer를 정의할 수 있습니다. 이는 주로 기본 cmp.Diff()만으로 비교하기 어려운 type에 사용합니다.&lt;/p&gt;
&lt;p&gt;예를 들면 decimal의 경우 decimal이 표현하는 scale까지 Equal로 비교하는 경우가 있는데, 이 경우 &lt;code&gt;1000000.00&lt;/code&gt;과 &lt;code&gt;1000000.000&lt;/code&gt;을 &lt;code&gt;1000000&lt;/code&gt;이라는 같은 값으로 보고 싶지만, Equal 비교 시 ‘서로 다르다’는 결과를 반환할 수 있습니다. 그래서 아래와 같이 Custom Comparer를 정의하면 이를 의도한 대로 비교할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
  &lt;span&gt;"testing"&lt;/span&gt;
  
	&lt;span&gt;"github.com/ericlagergren/decimal"&lt;/span&gt;
	&lt;span&gt;"github.com/google/go-cmp/cmp"&lt;/span&gt;
	&lt;span&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;// DecimalComparer cmp.Diff 옵션으로 사용할 수 있는 decimal.Big Comparer를 반환합니다.&lt;/span&gt;
&lt;span&gt;// [usecase]&lt;/span&gt;
&lt;span&gt;// assert.Fail(t, "expected not equal to actual (-expected +actual)", diff)&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;DecimalComparer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; cmp&lt;span&gt;.&lt;/span&gt;Option &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; cmp&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Comparer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;a&lt;span&gt;,&lt;/span&gt; b &lt;span&gt;*&lt;/span&gt;decimal&lt;span&gt;.&lt;/span&gt;Big&lt;span&gt;)&lt;/span&gt; &lt;span&gt;bool&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;if&lt;/span&gt; a &lt;span&gt;==&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; b &lt;span&gt;==&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
			&lt;span&gt;return&lt;/span&gt; a &lt;span&gt;==&lt;/span&gt; b
		&lt;span&gt;}&lt;/span&gt;

		&lt;span&gt;return&lt;/span&gt; a&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Cmp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;b&lt;span&gt;)&lt;/span&gt; &lt;span&gt;==&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestCreateUserAccount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;// given&lt;/span&gt;
	&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
		accountName &lt;span&gt;=&lt;/span&gt; &lt;span&gt;"Grace"&lt;/span&gt;
		balance     &lt;span&gt;=&lt;/span&gt; decimal&lt;span&gt;.&lt;/span&gt;&lt;span&gt;New&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1000000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;// 10,000.00&lt;/span&gt;
	&lt;span&gt;)&lt;/span&gt;

	&lt;span&gt;// when&lt;/span&gt;
	account&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;CreateUserAccount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;accountName&lt;span&gt;,&lt;/span&gt; balance&lt;span&gt;)&lt;/span&gt;

	&lt;span&gt;// then&lt;/span&gt;
	require&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; err&lt;span&gt;)&lt;/span&gt;
	expectedAccount &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;UserAccount&lt;span&gt;{&lt;/span&gt;
		Name&lt;span&gt;:&lt;/span&gt;    &lt;span&gt;"Grace"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
		&lt;span&gt;// decimal이 표현하는 scale은 다르지만 value는 같음&lt;/span&gt;
		Balance&lt;span&gt;:&lt;/span&gt; decimal&lt;span&gt;.&lt;/span&gt;&lt;span&gt;New&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;10000000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;3&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;// 10,000.000&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
	
	&lt;span&gt;// assert.Equal(t, expectedAccount, account) -&amp;gt; fail&lt;/span&gt;
	
	&lt;span&gt;// DecimalComparer를 Diff() 옵션으로 추가&lt;/span&gt;
	&lt;span&gt;if&lt;/span&gt; diff &lt;span&gt;:=&lt;/span&gt; cmp&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Diff&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;expectedAccount&lt;span&gt;,&lt;/span&gt; account&lt;span&gt;,&lt;/span&gt; &lt;span&gt;DecimalComparer&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; diff &lt;span&gt;!=&lt;/span&gt; &lt;span&gt;""&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		assert&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Fail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"expected not equal to actual (-expected +actual)"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; diff&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;Table Driven Test&lt;/strong&gt;는 단순함이 장점이지만 실무에서는 다양한 의존성과 mock 코드가 늘어나면 사용하기 어려워집니다. 그래서 단순한 유틸 함수의 경우 Table Driven Test를 주로 활용하고 비즈니스 로직 테스트와 같이 복잡하고 여러 mocking이 필요한 로직은 &lt;code&gt;stretchr/testify/suite&lt;/code&gt; package를 사용해 유지보수성을 챙길 수 있습니다.&lt;/p&gt;
&lt;p morss_own_score="7.0" morss_score="16.0"&gt;&lt;code&gt;stretchr/testify/suite&lt;/code&gt;은 아래와 같이 struct 기반의 테스트 구조를 제공하며, &lt;code&gt;suite.Suite&lt;/code&gt;를 임베딩한 struct 안에 다양한 의존성을 정의해둘 수 있습니다. 그리고 &lt;code&gt;SetupTest()&lt;/code&gt;, &lt;code&gt;SetupSubTest()&lt;/code&gt;, &lt;code&gt;TearDownTest()&lt;/code&gt;, &lt;code&gt;TearDownSubTest()&lt;/code&gt; 등 interface method를 통해 테스트 라이프사이클을 제어할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이런 구조를 통해 의존성 라이프 사이클 제어는 suite.Suite에서 담당하고 각 테스트에서는 의존성 선언을 따로 하지 않고 테스트 로직에 집중할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;"github.com/stretchr/testify/suite"&lt;/span&gt;
	&lt;span&gt;"github.com/golang/mock/gomock"&lt;/span&gt;
	&lt;span&gt;// ...&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; CreditUserServiceTestSuite &lt;span&gt;struct&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	suite&lt;span&gt;.&lt;/span&gt;Suite
	&lt;span&gt;// 여기에 의존성 정의&lt;/span&gt;
	ctrl &lt;span&gt;*&lt;/span&gt;gomock&lt;span&gt;.&lt;/span&gt;Controller &lt;span&gt;// fyi; gomock을 mocking에 사용&lt;/span&gt;
	userService &lt;span&gt;*&lt;/span&gt;user_app&lt;span&gt;.&lt;/span&gt;MockUserService
	creditUserRepository &lt;span&gt;*&lt;/span&gt;repository&lt;span&gt;.&lt;/span&gt;MockCreditUserRepository
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;KCBUserServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;SetupTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;// Test 실행 시마다 실행할 로직. (의존성 초기화 등)&lt;/span&gt;
	s&lt;span&gt;.&lt;/span&gt;ctrl &lt;span&gt;=&lt;/span&gt; gomock&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NewController&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
  s&lt;span&gt;.&lt;/span&gt;userService &lt;span&gt;=&lt;/span&gt; user_app&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NewMockUserService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;s&lt;span&gt;.&lt;/span&gt;ctrl&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;KCBUserServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;TearDownTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;// Test 종료 시마다 실행할 로직. (의존성 제거 등)&lt;/span&gt;
	s&lt;span&gt;.&lt;/span&gt;ctrl&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Finish&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 비즈니스 로직 테스트에서 사용한다면 아래와 같은 모습이 됩니다.
아래 코드를 통해서 실제로 &lt;code&gt;의존성 라이프사이클 관리&lt;/code&gt;는 suite.Suite에서 담당하고 각 Test는 의존성 선언 코드 없이 테스트 코드에 집중할 수 있는 걸 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;/* ... */&lt;/span&gt;
	&lt;span&gt;"github.com/golang/mock/gomock"&lt;/span&gt; 
	&lt;span&gt;"github.com/stretchr/testify/suite"&lt;/span&gt;
	&lt;span&gt;/* ... */&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;type&lt;/span&gt; TermsServiceTestSuite &lt;span&gt;struct&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	suite&lt;span&gt;.&lt;/span&gt;Suite
	ctrl &lt;span&gt;*&lt;/span&gt;gomock&lt;span&gt;.&lt;/span&gt;Controller

	termsCli &lt;span&gt;*&lt;/span&gt;client&lt;span&gt;.&lt;/span&gt;MockTermsClient
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;SetupTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	s&lt;span&gt;.&lt;/span&gt;ctrl &lt;span&gt;=&lt;/span&gt; gomock&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NewController&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

	s&lt;span&gt;.&lt;/span&gt;termsCli &lt;span&gt;=&lt;/span&gt; client&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NewMockTermsClient&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;s&lt;span&gt;.&lt;/span&gt;ctrl&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;TearDownTest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	s&lt;span&gt;.&lt;/span&gt;ctrl&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Finish&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;newService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;*&lt;/span&gt;termsService &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;termsService&lt;span&gt;{&lt;/span&gt;
		termsCli&lt;span&gt;:&lt;/span&gt; s&lt;span&gt;.&lt;/span&gt;termsCli&lt;span&gt;,&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;TestTermsServiceTestSuite&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t &lt;span&gt;*&lt;/span&gt;testing&lt;span&gt;.&lt;/span&gt;T&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	suite&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;t&lt;span&gt;,&lt;/span&gt; &lt;span&gt;new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;TestListUserTermsConsentStatusesByTermsTypes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
		now &lt;span&gt;=&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2024&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;6&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;12&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;UTC&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;)&lt;/span&gt;
	
	s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"여러_약관_유형을_조회할_때_모두_동의한_경우_해당_수만큼_약관_상태를_반환"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;// given&lt;/span&gt;
		ctx &lt;span&gt;:=&lt;/span&gt; context&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Background&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
		&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
			userID         &lt;span&gt;int64&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;123&lt;/span&gt;
			termsTypeEnums       &lt;span&gt;=&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;termspb&lt;span&gt;.&lt;/span&gt;TermsType&lt;span&gt;{&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
		&lt;span&gt;)&lt;/span&gt;

		s&lt;span&gt;.&lt;/span&gt;termsCli&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EXPECT&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;
		  &lt;span&gt;FindUserTermsConsentStatusesByTermsTypes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;
			&lt;span&gt;Return&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
			  &lt;span&gt;// 모두 동의한 상태면 request에 입력한 약관 유형 수와 동일한 수의 결과 반환&lt;/span&gt;
  			&lt;span&gt;/* ... */&lt;/span&gt;
  		&lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

		&lt;span&gt;// when&lt;/span&gt;
		termsService &lt;span&gt;:=&lt;/span&gt; s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;newService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
		result&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; termsService&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ListUserTermsConsentStatusesByTermsTypes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx&lt;span&gt;,&lt;/span&gt; userID&lt;span&gt;,&lt;/span&gt; termsTypeEnums&lt;span&gt;)&lt;/span&gt;

		&lt;span&gt;// then&lt;/span&gt;
		s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;
		s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Equal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;termsTypeEnum&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;TermsTypeEnum&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"반환된 약관 유형이 요청한 약관 유형과 동일해야 함"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	
	&lt;span&gt;// 케이스 추가 시 아래에 s.Run()과 함께 테스트 코드 추가&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 다른 Test 함수에서도 TermsServiceTestSuite에 정의한 의존성 사용 가능&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;s &lt;span&gt;*&lt;/span&gt;TermsServiceTestSuite&lt;span&gt;)&lt;/span&gt; &lt;span&gt;TestGetUserTermsConsentStatusByTermsType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
		now &lt;span&gt;=&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2024&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;6&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;12&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; time&lt;span&gt;.&lt;/span&gt;UTC&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;)&lt;/span&gt;
	
	s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"단일_약관_유형을_조회할_때_동의한_경우_약관_상태를_반환"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;// given&lt;/span&gt;
		ctx &lt;span&gt;:=&lt;/span&gt; context&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Background&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
		&lt;span&gt;var&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;
			userID        &lt;span&gt;int64&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;123&lt;/span&gt;
			termsTypeEnum       &lt;span&gt;=&lt;/span&gt; &lt;span&gt;/* ... */&lt;/span&gt;
		&lt;span&gt;)&lt;/span&gt;

		s&lt;span&gt;.&lt;/span&gt;termsCli&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EXPECT&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;
		  &lt;span&gt;FindOneUserTermsConsentStatusByTermsType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;
		  &lt;span&gt;Return&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/* ... */&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;nil&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

		&lt;span&gt;// when&lt;/span&gt;
		termsService &lt;span&gt;:=&lt;/span&gt; s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;newService&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
		result&lt;span&gt;,&lt;/span&gt; err &lt;span&gt;:=&lt;/span&gt; termsService&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GetUserTermsConsentStatusByTermsType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx&lt;span&gt;,&lt;/span&gt; userID&lt;span&gt;,&lt;/span&gt; termsTypeEnum&lt;span&gt;)&lt;/span&gt;

		&lt;span&gt;// then&lt;/span&gt;
		s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NoError&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;err&lt;span&gt;)&lt;/span&gt;
		s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;NotNil&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;result&lt;span&gt;)&lt;/span&gt;
		s&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Equal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;termsTypeEnum&lt;span&gt;,&lt;/span&gt; result&lt;span&gt;.&lt;/span&gt;TermsTypeEnum&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"반환된 약관 유형이 요청한 약관 유형과 동일해야 함"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이 글에서는 테스트란 &lt;code&gt;피드백 순환 고리&lt;/code&gt;(Feedback Loop)를 만들어 ‘불확실성’을 대비하고, 더 나아가 소프트웨어의 품질을 안정적으로 유지하면서 비즈니스 리스크를 최소화하기 위한 것이라는 관점을 표현하려고 했습니다. 그래서 테스트의 본질부터 이야기를 시작했습니다.&lt;/p&gt;
&lt;p&gt;그리고 글의 뒷부분에서는 실질적으로 Go 언어에서 생산성을 높이는 Table Driven Test, &lt;code&gt;stretchr/testify&lt;/code&gt;의 assertion 유틸과 struct style test, 그리고 가독성 높은 diff를 위한 &lt;code&gt;google/go-cmp&lt;/code&gt;까지 구체적인 도구들을 살펴보았는데요, 코드와 도구에 중점을 두었지만 여기서 다룬 도구 외에도 테스트에 도움을 주는 패턴 등 다양한 도구가 존재합니다. 그리고 그 도구가 탄생한 관점을 들여다보고 인사이트를 얻는 것이 중요하다고 이야기하고 싶습니다.&lt;/p&gt;
&lt;p&gt;추가로, 이 글에서 자세히 설명하진 않았지만 작성된 테스트 코드에 &lt;code&gt;Given, When, Then&lt;/code&gt; 구조를 일부 사용했습니다. 이 구조는 테스트의 ‘의도’를 명확하게 표현할 수 있게 도와줍니다.
이 구조는 장기적인 가독성과 유지보수성을 높이는 데 긍정적이라고 생각되고 명확하게 구조화된 테스트는 단순한 검증 도구를 넘어, 코드가 아닌 ‘비즈니스 요구사항(스펙)’ 자체를 설명하는 훌륭한 문서가 되어준다고 생각합니다. (이 관점에 대해서는 다른 글로 이야기하고 싶어서 이 글에서는 자세하게 다루지 않았습니다)&lt;/p&gt;
&lt;p&gt;그리고 이런 관점이 모이면, 엔지니어로서 시스템 안정성 확보와 비즈니스의 속도를 지키는 일이 단순히 기술적인 숙련도 이상의 의미를 가지게 되는 것 같습니다. 이는 우리가 해결하고자 하는 근본적인 비즈니스 문제를 깊이 이해하고 개발에만 치중하는 것이 아닌 비즈니스에도 집중하는 ‘메이커’의 태도와도 연결된다고 생각합니다.&lt;/p&gt;
&lt;p&gt;여러분은 이 글을 읽으면서 어떤 생각이 드셨나요? 나중에 기회가 된다면 함께 나누는 시간이 있길 기대합니다. 🙂&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;현재 뱅크샐러드에서는 &lt;strong&gt;대규모 채용 프로모션을 진행하고 있습니다!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저희 팀에서도 데이터와 함께 고객분들의 삶을 이롭게 하는 다양한 과제를 앞두고 있고 견고한 기술을 바탕으로 더 안전하고 빠른 비즈니스 여정, &lt;strong&gt;NEXT WAVE&lt;/strong&gt;를 함께 만들어갈 분들을 환영합니다. 🙌&lt;/p&gt;
&lt;p&gt;문을 두드리다 보면 열릴 것임을 믿으며 기대하고, 앞으로 합류하게 되실 분들과 함께 만들어갈 멋진 임팩트를 기대합니다. ✨&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</ns0:encoded></item><item><title>뱅크샐러드에서 합법적으로 Vibe Coding 하는 법</title><link>https://blog.banksalad.com/tech/banksalad-vibe-coding/</link><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="postDetailsstyle__PostDetailsWrapper-sc-r1ppdr-0 jKrqzo post_details" morss_own_score="5.673469387755103" morss_score="16.493694106856225"&gt;&lt;h1&gt;뱅크샐러드에서 합법적으로 Vibe Coding 하는 법&lt;/h1&gt;&lt;span&gt;05 Sep, 2025&lt;/span&gt;&lt;img src="data:image/svg+xml;charset=utf-8,%3Csvg%20height='780'%20width='1170'%20xmlns='http://www.w3.org/2000/svg'%20version='1.1'%3E%3C/svg%3E"&gt;&lt;img src="https://blog.banksalad.com/static/3a7094c917f00d18cfe2a67d5beebddc/a4cf4/thum_01.jpg"&gt;&lt;div class="postDetailsstyle__PostDescription-sc-r1ppdr-5 fTgjCK post_des" morss_own_score="5.640449438202247" morss_score="180.68955658105938"&gt;&lt;br&gt;
&lt;br&gt;
&lt;p&gt;안녕하세요. 혹시 뱅크샐러드에서 최근에 출시한 샐러드게임을 즐겨보셨나요? &lt;a href="https://www.banksalad.com/articles/%EC%83%90%EB%9F%AC%EB%93%9C%EA%B2%8C%EC%9E%84-%EC%A7%80%EC%B6%9C%EA%B4%80%EB%A6%AC-%EA%B7%9C%EC%B9%99-%EC%83%81%EA%B8%88-%ED%98%9C%ED%83%9D"&gt;샐러드게임은 일주일동안 지출내역을 바탕으로한 다양한 미션을 수행하며 지출통제를 하는 보람을 얻고, 성공을 통했을 시의 보상을 얻고, 그 과정에서 친구들과의 추억을 얻을 수 있도록 디자인된 게임입니다.&lt;/a&gt; 기본적인 구조는 일주일 동안 5명의 팀원이 주어진 예산 안에서 소비를 하되, 매일 특정한 &lt;strong&gt;미션&lt;/strong&gt;, 예컨대 &lt;code&gt;편의점에 소비하지 않기&lt;/code&gt;등의 미션을 수행해서 추가 예산을 얻거나 실패시엔 예산을 빼앗기는 방식으로 구성됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;여느 게임과 마찬가지로, 샐러드게임에서도 사용자들에게 지속적인 재미와 몰입감을 제공하려면 새로운 미션과 규칙을 끊임없이 업데이트하는 것이 중요했습니다. 예컨대 하루에 만원의 예산으로 버티는 &lt;code&gt;만원의 행복&lt;/code&gt; 미션이 있다면, 이를 &lt;code&gt;천원의 행복&lt;/code&gt;으로 만들어 난이도를 높이거나, &lt;code&gt;3만원의 행복&lt;/code&gt;으로 만들어 난이도를 낮출 수 있어야 했습니다.
그리고 많은 논의 끝에, 저희는 결국 샐러드게임에서도 여느 게임처럼, 최대한 개발없이 운영자가 다양한 시도를 할 수 있는 환경을 만들어야 한다는 결론에 이르렀습니다.&lt;/p&gt;
&lt;p&gt;가장 이상적인 그림은 운영자가 직접 새로운 게임 규칙을 만들고 적용하는 것이었습니다. 어차피 코드는 LLM이 짜주는 Vibe Coding의 시대. 운영자가 직접 코드를 짜게 하면 어떨까 생각했습니다. 이는 이론상은 가능하겠지만, LLM이 어떤 취약점을 일으키는 코드를  만들어낼지 보장할 수 없고, 따라서 결국 기존 엔지니어들이 코드리뷰를 해야하고, 그렇게 된다면 특별히 생산성의 개선이 있다고 말할 수는 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;운영자가 충분히 자유롭게 샐러드게임을 구성할 수 있어야 한다&lt;/code&gt;는 요구사항과, &lt;code&gt;운영자가 터무니없이 자유로워서는 안된다&lt;/code&gt;는 제약사항 사이에서 저희는 딜레마에 빠졌습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p morss_own_score="7.0" morss_score="13.5"&gt;저희는 이 문제를 해결하기 위해 ’&lt;strong&gt;샐러드게임 DSL(Domain Specific Language)&lt;/strong&gt;‘을 만들기로 결정했습니다. 샐러드게임의 규칙은 결국 특정 기간의 지출 내역이라는 정해진 입력에 대해 &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;reduce&lt;/code&gt;와 같은 함수만을 적용하는 형태라는 점에 주목했습니다. 이러한 제한적인 연산만을 허용하는 DSL로 만들어진 코드라면 운영자가 직접 작성하더라도 시스템의 안정성을 해치지 않을 수 있다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;DSL을 만든다고 하면 거창해보이지만, 사실은 생각보다 어렵지 않았습니다. GitLab에서 만든 &lt;a href="https://gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework"&gt;micro-language-framework&lt;/a&gt; 같은 프레임워크를 활용하면, 토큰 파싱과 같은 DSL 제작의 어려운 부분을 프레임워크에 맡기고, 저희에게 필요한 연산자와 함수의 구현만을 플러그인 형태로 추가할 수 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;type&lt;/span&gt; FunctionLTE &lt;span&gt;struct&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// (:desc) 명령이 출력할, 해당 연산의 설명&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;f &lt;span&gt;*&lt;/span&gt;FunctionLTE&lt;span&gt;)&lt;/span&gt; &lt;span&gt;Desc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; fmt&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"(:&amp;gt; a (%s b))"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; f&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Symbol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"a 가 b 보다 값이 더 작거나 같은지 비교함"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 해당 연산을 표현할 토큰 라벨&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;f &lt;span&gt;*&lt;/span&gt;FunctionLTE&lt;span&gt;)&lt;/span&gt; &lt;span&gt;Symbol&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; parser&lt;span&gt;.&lt;/span&gt;TokLabel &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; parser&lt;span&gt;.&lt;/span&gt;&lt;span&gt;TokLabel&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"&amp;lt;="&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 해당 연산의 타당성 검토&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;f &lt;span&gt;*&lt;/span&gt;FunctionLTE&lt;span&gt;)&lt;/span&gt; &lt;span&gt;Validate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;env &lt;span&gt;*&lt;/span&gt;eval&lt;span&gt;.&lt;/span&gt;Environment&lt;span&gt;,&lt;/span&gt; stack &lt;span&gt;*&lt;/span&gt;eval&lt;span&gt;.&lt;/span&gt;StackFrame&lt;span&gt;)&lt;/span&gt; &lt;span&gt;error&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;validateComparsionOps&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;f&lt;span&gt;,&lt;/span&gt; stack&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 해당 연산의 실행&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;f &lt;span&gt;*&lt;/span&gt;FunctionLTE&lt;span&gt;)&lt;/span&gt; &lt;span&gt;Evaluate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;env &lt;span&gt;*&lt;/span&gt;eval&lt;span&gt;.&lt;/span&gt;Environment&lt;span&gt;,&lt;/span&gt; stack &lt;span&gt;*&lt;/span&gt;eval&lt;span&gt;.&lt;/span&gt;StackFrame&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;eval&lt;span&gt;.&lt;/span&gt;Result&lt;span&gt;,&lt;/span&gt; &lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; &lt;span&gt;evaluateComparsionOps&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;stack&lt;span&gt;,&lt;/span&gt; &lt;span&gt;func&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;i&lt;span&gt;,&lt;/span&gt; j &lt;span&gt;int&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;bool&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
		&lt;span&gt;return&lt;/span&gt; i &lt;span&gt;&amp;lt;=&lt;/span&gt; j
	&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;figcaption&gt;desc, symbol, validate, evaluate 등의 인터페이스만 구현하면 DSL 명세를 구성하는 것은 어려운 일이 아니었습니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;한 편, 제가 여러 백오피스를 만들며 얻은 가장 중요한 교훈 하나를 꼽자면, 바로 백오피스에서 영어사용은 최소화 해야 한다는 것이었습니다. 영어가 딱 들어가는 순간, 사용자는 그 영역을 개발자들이 쓰는 곳으로 생각하고, 그 기능을 두렵게, 내지는 어렵게 받아들여 실제로 사용하기 힘들어하는 경향이 있었습니다. ‘마지막 업데이트 날짜’같은 사소한 레이블이 영어여도 이런 경향이 발생하는데, 하물며 일종의 코딩을 하게 만드는 DSL에 한글을 쓸 수 없다면, 이런 경향은 더 심화될 것으로 봤습니다. 그래서 ‘한글 사용 가능성’은 DSL 문법에서 상당히 중요한 요구사항이었습니다.&lt;/p&gt;
&lt;p&gt;그러나 당시 &lt;a href="https://gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework"&gt;micro-language-framework&lt;/a&gt; 에선 한글 토큰을 지원하지 않고 있었기에, 이를 지원하도록 하는 &lt;a href="https://gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework/-/merge_requests/13"&gt;기여&lt;/a&gt;를 함께 진행했습니다.&lt;/p&gt;
&lt;p&gt;최종적으로 저희는 다음과 같은 형식의 DSL을 선언, 구성할 수 있게 되었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 노커피 미션의 성공조건. 그룹 전체에서 소비한 내역의 브랜드 중에 카페 브랜드가 하나라도 포함되면 실패
(:&amp;gt; 그룹소비브랜드목록 (하나라도포함 카페브랜드목록) (== false))
&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;하지만 DSL 문법을 만들었다고 하더라도, 여전히 일반운영자가 그 문법을 파악하여 사용하는 것은 쉽지 않은 일이었습니다. 그래서 저희는 운영자가 한글로 된 미션규칙의 설명을 입력하면, 그 DSL의 생성은 LLM이 할 수 있어야 한다고 봤습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/a4b920569aed8f01430389f688c4a321/e011a/1_01.png"&gt;

&lt;img title="1_01" src="https://blog.banksalad.com/static/a4b920569aed8f01430389f688c4a321/663f3/1_01.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt;샐러드게임 백오피스 
한글로된 미션 설명을 입력하면, 번역버튼을 눌러서 LLM이 DSL을 제안할 수 있도록 해야했습니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이를 위해서는 LLM에게 다음두 가지 맥락을 전달할 필요가 있었습니다.&lt;/p&gt;
&lt;p&gt;첫째는 DSL의 문법 및 주요 연산자들에 대한 내용이었습니다. 다행히  &lt;a href="https://gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework"&gt;micro-language-framework&lt;/a&gt; 에서는 &lt;a href="https://gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework#core-language"&gt;desc&lt;/a&gt; 라는 기본 함수를 제공해, 우리가 만든 DSL의 전체 스펙을 한 번에 텍스트 형태로 출력하는 기능을 지원했습니다. 이를 그대로 LLM 프롬프트에 포함하여 LLM이 DSL 문법을 알게 하는 문제는 쉽게 해결되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/7bdf05b49985d67075b8e699a2aef54c/e26aa/1_02.png"&gt;

&lt;img title="1_02" src="https://blog.banksalad.com/static/7bdf05b49985d67075b8e699a2aef54c/663f3/1_02.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt;micro-language-framework에서 기본으로 제공하는 desc 명령어 실행 결과
우리가 만든 스펙을 plug-in 하면, 그 스펙에서 우리가 선언한 desc이 함께 출력됩니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;두번째는 사용사례에 대한 내용이었습니다. 주어진 미션규칙을 가장 높은 가독성으로 표현한 DSL의 사례들을 LLM에게 전달해야 비슷한 품질의, 그리고 무엇보다도 예측 가능하고 일관성 있는 DSL을 생성할 수 있었습니다.  이 역시, 백오피스에서 새로운 미션을 만들 때마다 DB에 쌓이는 사용 사례들을 LLM이 학습할 수 있도록 제공하여 쉽게 해결할 수 있었습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;import&lt;/span&gt; 	&lt;span&gt;"gitlab.com/gitlab-org/vulnerability-research/foss/micro-language-framework/eval"&lt;/span&gt;

&lt;span&gt;func&lt;/span&gt; &lt;span&gt;saladgameSpecPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;ctx context&lt;span&gt;.&lt;/span&gt;Context&lt;span&gt;,&lt;/span&gt; readOnlyDB &lt;span&gt;*&lt;/span&gt;sql&lt;span&gt;.&lt;/span&gt;DB&lt;span&gt;)&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	prompt &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;`
  다음은 샐러드게임 DSL의 스펙입니다.
  주요 연산자는 다음과 같습니다.
  ---
  `&lt;/span&gt;
  &lt;span&gt;// micro-language-frmaework 에서 제공하는, DSL 연산자들에 대한 설명 출력 기능&lt;/span&gt;
	desc&lt;span&gt;,&lt;/span&gt; &lt;span&gt;_&lt;/span&gt; &lt;span&gt;:=&lt;/span&gt; eval&lt;span&gt;.&lt;/span&gt;&lt;span&gt;EvaluateExpression&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"(desc)"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	prompt &lt;span&gt;+=&lt;/span&gt; desc&lt;span&gt;.&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
  예제 dsl은 다음과 같습니다.
  ---
  `&lt;/span&gt;
  &lt;span&gt;// 서버에 저장된 현재 사용중인 DSL 의 사례들&lt;/span&gt;
	existingDsls &lt;span&gt;:=&lt;/span&gt; repository&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getExistingDSLs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;for&lt;/span&gt; &lt;span&gt;_&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; dsl &lt;span&gt;:=&lt;/span&gt; &lt;span&gt;range&lt;/span&gt; existingDsls &lt;span&gt;{&lt;/span&gt;
		basePrompt &lt;span&gt;+=&lt;/span&gt; fmt&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`
    // %s
    dsl = '%s'
    `&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; dsl&lt;span&gt;.&lt;/span&gt;설명&lt;span&gt;,&lt;/span&gt; dsl&lt;span&gt;.&lt;/span&gt;구현&lt;span&gt;)&lt;/span&gt;
	&lt;span&gt;}&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
  ---
  이상의 '샐러드게임 DSL' 스펙을 충분히 숙지하세요.
  `&lt;/span&gt;
	&lt;span&gt;return&lt;/span&gt; prompt
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figcaption&gt;샐러드게임 DSL 생성 프롬프트. DSL의 문법과 사용사례를 prompt 에 함께 제공합니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;여기까지는 순조로웠지만, LLM 활용에서 가장 어려운 문제 중 하나는 바로 ‘환각(Hallucination)‘을 극복하는 것이었습니다. LLM은 ‘만들 수 있는 것’을 만드는 데는 탁월하지만, ‘만들 수 없는 것’을 만들 수 없다고 말하는 데는 취약합니다. LLM은 종종 DSL 스펙에서 지원하지 않는 기능임에도 불구하고, 마치 지원하는 것처럼 꾸며내곤 했습니다.&lt;/p&gt;
&lt;p&gt;저희는 이러한 환각 효과를 막기 위한 근본적 안전장치로 &lt;code&gt;TestSaladgameDSL&lt;/code&gt;이라는 별도의 API를 개발했습니다. 이 API를 통해 생성된 DSL 코드가 특정 입력 상황에서 기대하는 결과를 정확히 출력하는지 백오피스에서 즉시 테스트할 수 있도록 했습니다. 어쩌니 저쩌니 해도, LLM이 만들었건 사람이 만들었건, 만들어진 코드에 대해서 단위테스트를 붙이는 일은 필수인 것입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/c129d21e68fc88e30feaac15b0d5576b/f0f3e/1_03.png"&gt;

&lt;img title="1_03" src="https://blog.banksalad.com/static/c129d21e68fc88e30feaac15b0d5576b/663f3/1_03.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt;DSL을 직접 테스트케이스로 실행시킴으로써, 해당 DSL의 문법오류는 없는지, 기대대로 동작하는지등을 테스트 할 수 있습니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;최종적으로는 테스트를 통해 안정성을 확보하더라도, 애초에 환각 자체가 적거나 없는 것이 최선이라고 봤습니다. 그래서 저희는 환각을 줄이기 위해 ‘이중 검토’ 방식을 도입했습니다. 첫 번째 LLM 프롬프트로 DSL을 생성한 후, 두 번째 LLM 프롬프트에 “경쟁사 LLM이 이렇게 주장했어. 비판적으로 검토해봐”와 같은 질문을 던져 생성된 DSL을 한 번 더 검증했습니다. 이 방식은 API 호출 횟수를 늘려 레이턴시가 증가했지만, 환각 효과를 획기적으로 줄이는 데 기여했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;// 1차 DSL 생성 &lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;dslGenerationPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userRequest &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	prompt &lt;span&gt;:=&lt;/span&gt; fmt&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`
'샐러드게임 DSL' 스펙을 바탕으로, 유저가 입력한 문장 '%s' 를 '샐러드게임 DSL' 로 표현할 수 있는지 차근 차근 생각해보세요.
`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; userRequest&lt;span&gt;)&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
  만약 문장을 DSL로 표현할 수 있다면 해당 DSL을 적으세요.
  그리고 왜 이 문장이 해당 DSL로 표현될 수 있는지를 설명하세요.
  이 때 다음과 같은 구조로 답하세요
  `&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;outputStructurePrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

	&lt;span&gt;return&lt;/span&gt; prompt
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;// 2차 검수&lt;/span&gt;
&lt;span&gt;func&lt;/span&gt; &lt;span&gt;dslValidationPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userRequest&lt;span&gt;,&lt;/span&gt; specPrompt&lt;span&gt;,&lt;/span&gt; generatedDSL &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
	prompt &lt;span&gt;:=&lt;/span&gt; specPrompt
	prompt &lt;span&gt;+=&lt;/span&gt; fmt&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
	&lt;span&gt;`유저가 요청한 문장 %s 를 샐러드게임 DSL로 변환할 수 있겠냐는 질의에`&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
	 userRequest
	&lt;span&gt;)&lt;/span&gt;
	prompt &lt;span&gt;+=&lt;/span&gt; fmt&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sprintf&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;
	 &lt;span&gt;`경쟁사 AI가 다음과 같은 응답을 내놓았습니다. %s
   `&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; generatedDSL&lt;span&gt;)&lt;/span&gt;
	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
	해당 응답이 적절한지 차근차근 충분히 검토해주세요. 
	논리적인 문제가 있다면, 이를 수정해주세요.`&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
	만약 검토결과 유저가 요청한 문장을 DSL로 타당하게 바꿀 수 있다면, 
	다음의 양식으로 답변해주세요.`&lt;/span&gt;
	&lt;span&gt;...&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;p&gt;나아가, DSL을 생성할 때 이를 테스트할 테스트케이스도 함께 제안하도록 LLM에게 요청했습니다. 특히 이 과정에서 LLM에게 QA 엔지니어로서의 롤을 부여하여, 테스트가 집중되어야 할 경계 조건 등을 테스트케이스에 담을 수 있도록 유도했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;func&lt;/span&gt; &lt;span&gt;dslValidationPrompt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;userRequest&lt;span&gt;,&lt;/span&gt; specPrompt&lt;span&gt;,&lt;/span&gt; generatedDSL &lt;span&gt;string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;string&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
&lt;span&gt;...&lt;/span&gt;

	prompt &lt;span&gt;+=&lt;/span&gt; &lt;span&gt;`
	여기에 더해, 생성된 DSL이 유저가 의도한 내용과 같은지를 
	다음과 같은 형식으로 유저에게 물어보세요. 
	숙련된 QA 엔지니어로서 경계조건을 충분히 고려하세요`&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;
&lt;br&gt;

&lt;a href="https://blog.banksalad.com/static/7bdf05b49985d67075b8e699a2aef54c/e26aa/1_02.png"&gt;

&lt;img title="1_02" src="https://blog.banksalad.com/static/7bdf05b49985d67075b8e699a2aef54c/663f3/1_02.png"&gt;
&lt;/a&gt;

&lt;figcaption&gt;micro-language-framework에서 기본으로 제공하는 desc 명령어 실행 결과
우리가 만든 스펙을 plug-in 하면, 그 스펙에서 우리가 선언한 desc이 함께 출력됩니다.&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;마지막으로, 이렇게 만들어진 미션이 최종적으로 QA엔지니어의 엄격한 수동 테스트를 거치면, 해당 미션은 사용자를 만나게 될 준비를 마치게 됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;LLM 시대의 엔지니어링은, 역설적이게도 &lt;code&gt;어떻게 토큰 수를 절약할 것인가&lt;/code&gt;, 즉 &lt;code&gt;어떻게 LLM을 **덜** 쓸 것인가&lt;/code&gt; 의 문제로 귀결될 것입니다. 그런 의미에서, 한 번의 LLM 실행의 결과를 안정적인 형태로, 예측가능한 범위 안에서 여러번 활용할 수 있게 만드는 DSL 레이어가 매우 중요한 장치가 될 수 있을 것으로 생각합니다. 이를 통해 LLM만이 가능하게 만드는 유연성을 얻으면서도, 토큰의 절약과 결과물의 예측가능성을 동시에 잡을 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;지금까지 엔지니어들의 역할이 API를 만드는 것이었다면, 어쩌면 미래의 엔지니어들의 주 과업은 LLM이 마음껏 안전하게 Vibe Coding을 할 수 있는 환경, 예컨대 DSL을 설계하고 검증하는 시스템 등을 만드는 것이 되지 않을까 하는 예상을 해봅니다.&lt;/p&gt;
&lt;p&gt;아직은 뚜렷하지 않은 이 미래의 개발 환경에, 뱅크샐러드와 함께 한 걸음 먼저 내딛을 분이 계시다면, 언제든 아래 링크를 눌러주시기 바랍니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</ns0:encoded></item></channel></rss>